trashpandafandomcom-20200213-history
Monads
What are they? A monad is a data type (e.g int) that encapsulates some control flow (e.g try/catch). Why are they important? The key to understanding the benefit of monads is realizing expressions themselves don't cause exceptions, the evaluation causes exceptions. If you can resolve your expression to a type that can be evaluated later, you may be able to handle exceptions as part of a type definition, which could provide a handle for static analysis tools or compile-time guarantees to hook into. So monads can help us implement a safer version of a expression without suffering many of the negative side effects that come with loading up our program with a bunch of different try/except statements. For example: def div(a, b): try: return a / b except ZeroDivisionError: pass # WHAT TO DO HERE?? # General catch-all for unhandled # edge cases except Exception as err: raise err The above would end up leading to many different except '''clauses, which would expand our '''try/catch block with respect to the number of exception types you want to catch. Also if you apply exception handling as part of any mainline program execution, you lose the ability to accurately trace and exceptions from the handling logic and before. Maybe, Just, Nothing Here is the initial definition of the Maybe, Just, and Nothing '''monads: class _Maybe(object): This class definition exists to add any monadic attributes or operators. Since we are only talking about '''Maybe, Nothing, and Just, and since Nothing '''and '''Just '''inherit from '''Maybe, we'll wrap any basic and monadic attributes as part of this class definition. def Maybe(cls): raise NotImplentedError Parametrization of a typedef to include monadic attributes. This method enables dynamic classdef generation by inheriting a base class passed in as an input argument and overriding class attribute '__base__' class _Just((_Maybe): # A successful evaluation of a Maybe monad. def Just(cls): # Functional instantiation of Just return type('Just(%s) % cls.__name__, (_Just, cls), {}) def _Nothing(_Maybe): # A failed evaluation of a Maybe monad. # Ensure that `Nothing` is unique; uniqueness implies same `id()` result, which # implies a global object (singleton), much like `None = type(None)()` is a # singleton. # >>> id(type(None)()) # 4562745448 # Some object ID, this may vary. # >>> id(None) # 4562745448 # Same object ID as before. # >>> Nothing = _Nothing() This results in the following: >>> Just(int)(1) 1 >>> type(Just(int)(1)) float: return a / b Can be turned into this: def safediv(a : Maybe(int), b : Maybe(int)) -> Maybe(float): if ( a is Nothing or b is Nothing or b 0 ): return Nothing return Just(float)(a / b) The type parametrization aspect of monads (at least in our above context), creates a bound on how a portion of control flow should fail: it simply returns Nothing. Because it's a type that represents an error, and because calling a monadic method with Nothing '''generates '''Nothing (enforcing idempotency), the runtime doesn't need to worry about having to raise an exception to avoid propagating an error that cannot be handled later on. As long as the types match, it is safe to run. Errors become safe to pipeline, and pipelining allows systems of arbitrary complexity to be developed. This is powerful because it linearizes your error model. No matter how complex your pipeline may get, you should only ever expect to get more Nothing types with a longer pipeline, and not totally new types of errors. You may think that this makes debugging harder because all failures are of type Nothing. This isn't necessarily true. Not only can you log the error message/input arguments/metadata to reproduce the error as part of the monad definition, you can also create new monad definitions whenever you want. : The '''Maybe' type is sometimes used to represent a value which is either correct or an error; by convention, the Nothing '''constructor is used to hold an error value and the '''Just constructor is used to hold a correct value.'' Let's try implementing binding/sequencing attributes for out monad classes: class _Maybe(object): def __init__(self, data=None): self.data = data raise NotImplementedError def bind(self, function): raise NotImplementedError def __rshift__(self, function): if not isinstance(function, _Maybe): error_message = 'Can only pipeline monads.' raise TypeError(error_message) if not callable(function): function = lambda _ : function return self.bind(function) # ... class _Just(_Maybe): def __init__(self, data=None): self.data = data def bind(self, function): return function(self) # ... class _Nothing(_Maybe): """A failed evaluation of a Maybe monad. """ def __init__(self, _=None): def __str__(self): return "Nothing" def bind(self, _): return self References *https://bytes.yingw787.com/posts/2019/12/06/monads/